/*******************************************************************************
 * Copyright 2011-2012 Dik Grapendaal
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 ******************************************************************************/
package sh.grapendaal.tsuushin.module;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.Enumeration;

import org.osgi.framework.Bundle;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import sh.grapendaal.tsuushin.annotation.Callback;
import sh.grapendaal.tsuushin.annotation.InjectService;
import sh.grapendaal.tsuushin.annotation.Module;
import sh.grapendaal.tsuushin.annotation.Permission;
import sh.grapendaal.tsuushin.annotation.Start;
import sh.grapendaal.tsuushin.annotation.Stop;

import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;

@Singleton
public class BundleProcessor {
	private static final Logger LOGGER = LoggerFactory.getLogger(BundleProcessor.class);

	@Inject
	private Injector mainInjector;

	/**
	 * Loads and processes the given bundle for use as a module.
	 * 
	 * @param bundle
	 *            The bundle to load.
	 * @return A ModuleContainer for the given bundle which contains information
	 *         about the module inside the bundle.
	 */
	public ModuleContainer processBundle(Bundle bundle) {
		LOGGER.debug("New bundle: {}", bundle);
		Enumeration<URL> entries = bundle.findEntries("/", "*.class", true);

		if (entries == null || !entries.hasMoreElements()) {
			return null;
		}

		ModuleContainer container = mainInjector.getInstance(ModuleContainer.class);
		while (entries.hasMoreElements()) {
			URL entry = (URL) entries.nextElement();
			String file = entry.getFile();
			String className = file.replaceAll("/", ".").replaceAll("\\.class", "").replaceFirst("\\.", "");
			this.loadClass(bundle, className, container);
		}

		return container;
	}

	/**
	 * Loads a class from the bundle. It will also check for any annotations
	 * present.
	 * 
	 * @param bundle
	 *            The bundle we're loading.
	 * @param className
	 *            The class we have to load.
	 * @param container
	 *            The module container for this bundle.
	 */
	private void loadClass(Bundle bundle, String className, ModuleContainer container) {
		try {
			Class<?> loadedClass = bundle.loadClass(className);
			LOGGER.debug("Loaded class {}", loadedClass);

			this.processAnnotations(loadedClass, container);
		} catch (ClassNotFoundException e) {
			// This should not happen, since we get the class name from the
			// bundle.findEntries() method.
			LOGGER.error("Class {} not found", className, e);
		}
	}

	/**
	 * If this method finds the @Module annotation on the given class, then
	 * processing will continue and that class will be used to instantiate the
	 * module. This is also the reason the other annotations will only work on
	 * this class. All found annotations will be processed and added to the
	 * given ModuleContainer.
	 * 
	 * @param clazz
	 *            The class to process.
	 * @param container
	 *            The ModuleContainer to add found annotations to.
	 */
	public void processAnnotations(Class<?> clazz, ModuleContainer container) {
		if (clazz.isAnnotationPresent(Module.class)) {
			Module annotation = clazz.getAnnotation(Module.class);

			if (container.getMainClass() == null) {
				LOGGER.debug("Module entry point found: {}. Module author: {}, version: {}",
						new Object[] { clazz.getCanonicalName(), annotation.author(), annotation.version() });
				container.setMainClass(clazz);

				this.processClassAnnotations(clazz, container);
				this.processFieldAnnotations(clazz.getDeclaredFields(), container);
				this.processMethodAnnotations(clazz.getDeclaredMethods(), container);
			} else {
				LOGGER.warn("Multiple classes found with the @Module annotation. "
						+ "Will use the first found class as entry point. Discarding {}.", clazz.getCanonicalName());
			}
		}
	}

	private void processClassAnnotations(Class<?> clazz, ModuleContainer container) {
		if (clazz.isAnnotationPresent(Permission.class)) {
			Permission permissionAnn = clazz.getAnnotation(Permission.class);
			for (String permission : permissionAnn.names()) {
				LOGGER.debug("Registering permission: {}", permission);
				container.registerPermission(permission);
			}
		}
	}

	private void processFieldAnnotations(Field[] fields, ModuleContainer container) {
		for (Field field : fields) {
			if (field.isAnnotationPresent(InjectService.class)) {
				LOGGER.debug("Service wire field found: {}", field.getName());
				container.addServiceWire(field);
			}
		}
	}

	private void processMethodAnnotations(Method[] methods, ModuleContainer container) {
		for (Method method : methods) {
			if (method.isAnnotationPresent(Callback.class)) {
				Callback callbackAnn = method.getAnnotation(Callback.class);
				LOGGER.debug("Callback method found: {}", method.getName());

				container.addCallback(method, callbackAnn.type());
			}

			if (method.isAnnotationPresent(Start.class)) {
				if (container.getStartMethod() == null) {
					LOGGER.debug("Start method found: {}", method.getName());
					container.setStartMethod(method);
				} else {
					LOGGER.warn("Start method already set. Will keep the first one and discard this one: {}.",
							method.getName());
				}
			}

			if (method.isAnnotationPresent(Stop.class)) {
				if (container.getStopMethod() == null) {
					LOGGER.debug("Stop method found: {}", method.getName());
					container.setStopMethod(method);
				} else {
					LOGGER.warn("Stop method already set. Will keep the first one and discard this one: {}.",
							method.getName());
				}
			}
		}
	}
}
